lint: widen the vocabulary gate's face refusal to every published package and retire boot-refusal - #17056
Conversation
…ed package and retire boot-refusal `check-dispatcher-error-vocabulary` refused a site classified away from registration only under `packages/spec/src/` — the one package #16449 had measured. The #16404 ruling it enforces is about every code that ships in `dist`, so the refusal now covers every published workspace member's `src/`, enumerated from the manifests through the shared workspace parse and held against a floor so a broken enumeration refuses instead of going quiet. `packages/spec/src/` keeps the stricter verdict set. Outside it, `pending-registration` keeps a named, dated allowance owed to #8846, so the two rows whose disposition belongs to #8846 / #9460 do not red on a card that is not theirs; every run prints how many rows still stand on it. With the whole scan population inside the face, `boot-refusal` is a verdict no row can legally carry, so it retires from `CodeVerdict`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…den-vocabulary-gate-retire-boot-refusal
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 825136a678bd48bee15421848b748db4f287231c && git checkout 825136a678bd48bee15421848b748db4f287231c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 71629a1513bcddaa76bd3435d9f18559cc473bba 0292e14f7a7c3ff7a768414afb61d008baf9dcf8 && git checkout -B drift-repro 71629a1513bcddaa76bd3435d9f18559cc473bba && git merge --no-ff 0292e14f7a7c3ff7a768414afb61d008baf9dcf8
node scripts/docs-audit/affected-docs.mjs --json 71629a1513bcddaa76bd3435d9f18559cc473bba |
…e verdict `packages/spec` ships `src/**/*.zod.ts` in its `files[]`, so the ledger's TSDoc is published prose. One sentence said the dispatcher vocabulary's `boot-refusal` verdict records reachability for codes not yet registered and pointed at the file this branch deletes it from — a live-mechanism claim, not a case that can no longer arise, so a reader would follow the pointer and find nothing. It now records the retirement and names what replaced it. The paragraph below it claimed only `packages/spec/src/**` is held mechanically. That understated the rule after the widening, so it now states the published face, the stricter spec sub-face and the dated #8846 allowance, with both finding kinds named. The mirrored reference page is regenerated with `check:generated --fix` (`gen:docs`), never by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…den-vocabulary-gate-retire-boot-refusal
…den-vocabulary-gate-retire-boot-refusal
… main `os-regen-merge.sh` step 4. Both sides had changed the generated page, so step 2 restored main's side and this regenerates it from the merged source: the reference now carries #16783's three new rows and this branch's corrected TSDoc together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…den-vocabulary-gate-retire-boot-refusal
…workspace glob non-empty The changeset half is the binding correction: `packages/spec` ships `src/**/*.zod.ts`, so the ledger's TSDoc goes out in the tarball verbatim and a docs-only edit to it publishes. `skip-changeset` was wrong the moment that file entered the diff; this adds the `@objectstack/spec: patch` the lane's five precedents on this head all carry for the same shape. Also from the same review, non-binding: - The published face's floor could not catch a lost workspace glob and the docblock implied it could. Measured per glob on this tree: the largest is `packages/*` at 31 of 88 members, so losing it entirely still leaves 57 — over any floor low enough not to red on ordinary churn — and `expandWorkspaceGlob` returns `[]` for a vanished parent silently. `emptyWorkspaceGlobs` now refuses that case by name, the floor's docblock states what it can and cannot see, and a self-test case asserts the arithmetic in the direction that keeps the pin necessary. - The TSDoc said the gate refuses every verdict that parks a site away from registration, which over-generalises the allowance stated eight lines below it. It now names `pending-registration` and its scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Fixes #16649
Second half of the card. The first half (registering the fourteen
door: 'none'codes) landed as #16879 on 2026-09-08; this is the mechanical closure the card's Scope and triage acceptance items 3/4/5 ask for.Implemented per the director seat's ruling of 2026-09-08 (comment 5586454647, decision batch #95), quoted verbatim in the card. The later release comment 5586642296 re-lists options A/B/C as open; it was written thirteen minutes after the ruling and does not cite it. This PR follows the ruling; A/B/C were not re-litigated.
What changed
scripts/check-dispatcher-error-vocabulary.mjs— the face refusal, which askedfile.startsWith('packages/spec/src/')and nothing else, now asks which published package'ssrc/a site sits in. Members are enumerated from their manifests throughscripts/workspace-enumerator.mjs, the repo's one parse ofpnpm-workspace.yaml; "published" isname && private !== true, the same testcheck-published-files.mjsapplies, because the ruling's premise is about what a consumer installs. Measured on this tree: 80 workspace members, 70 published, 69 of those with asrc/on disk. The derivation emits one prefix per published member and does not probe the disk forsrc/, so the printed count is members, not members-with-sources; that is stated where it is derived.The two faces stay separate because they differ:
packages/spec/src/keepsforeign-vocabulary/runtime-pinnedonly, and the widened region additionally admitspending-registrationunder a named, dated allowance owed to #8846. The spec region keeps reportingspec-face-unregisteredand the widened region reports a newpublished-face-unregistered, so the ledger's existing prose about that kind stays true and the two regions stay distinguishable in output.The enumeration is floor-checked before use (
PUBLISHED_SOURCE_FACE_FLOOR = 40, against 70 measured). A face list that silently shrank would turn the refusal off for whatever fell out and print the same green line as a complete one, so the gate refuses instead of reporting.The
unclassified-siteremedy text told every author to reach forpending-registration. Insidepackages/spec/src/that is the one verdict the face refuses, so the text is now face-aware and names the ledger row first.packages/runtime/src/dispatcher-error-vocabulary.ts—boot-refusalretired fromCodeVerdict. With the whole scan population inside the face (52 sites: 47 published-face, 5 spec-face), no row can legally carry it. Its own doc comment had already named this as the condition for its retirement. A paragraph in its place records what it meant and why it must not come back; the running log records the fourth cycle closing mechanically rather than by having been emptied once.The ledger TSDoc, corrected (patch round)
packages/specshipssrc/**/*.zod.tsin itsfiles[], soerror-code-ledger.zod.ts's TSDoc is published prose, and retiring the verdict made part of it false rather than merely stale. Two blocks changed, both inside the:100-132docblock; the two regions contended by #17015 and #16783 were not touched.:109-113— false, and fixed. It said the dispatcher vocabulary'sboot-refusalverdict records reachability for codes not yet registered, and pointed at the file this branch deletes it from. That describes where a live mechanism lives, not a case that can no longer arise, so a reader would have followed the pointer and found nothing. It now records the retirement and names what replaced it: the gate refuses every verdict that parks a site away from registration under any published package'ssrc/, so adoor: 'none'code has no resting place short of a ledger row.:119-124— also changed, and here is why. The vacuous-disjunct argument does hold for it: with the verdict gone, "aboot-refusalorpending-registrationrow is a finding" keeps a true half. But that is not the only thing the paragraph claims. It openedpackages/spec/src/** is held to this mechanically, which after this PR understates what is enforced — a reader would conclude only the spec tree is guarded, which is exactly the "守了一部分 reads the same as 守住了" failure this card exists to close. So it now states the published face, the stricter spec sub-face and the dated #8846 allowance, with both finding kinds named.The mirrored reference page
content/docs/references/api/error-code-ledger.mdxwas regenerated withcheck:generated --fix(gen:docs), never hand-edited.The eight other
boot-refusalmentions in that file (:170,:500,:798,:824,:1060,:1105,:1126,:1203) were read and left: every one is past tense about what a now-deleted row recorded and ratcheted out, with the reading itself stated to live on the ledger row now. None asserts a live mechanism.Acceptance item 4: the red-before-green demonstration
Item 4 was written when the fourteen codes were unregistered, so "run the widened gate and watch it red on all fourteen" was the available proof.
git grep -c "verdict: 'boot-refusal'"now returns 0 — the first half consumed that proof. The equivalent was constructed instead, and it measures the delta rather than merely showing the gate can fail: one mutation, two gates, the same tree.packages/types/src/node.ts'sMODULE_NOT_FOUNDrow (a published package outsidepackages/spec/src/) was flipped fromforeign-vocabularytosandbox-authored— still a legalCodeVerdict, so nothing else catches it.origin/main's script, byte-for-byte (308374 bytes), run from this worktree'sscripts/so itsROOTresolves here[published-face-unregistered] packages/types/src/node.ts stamps unregistered code 'MODULE_NOT_FOUND' (objlit) ... classifies it 'sandbox-authored' — a verdict refused under a published package's src/Mutation proven on disk before either leg: HEAD blob
0a1e68f2..., mutated blob26b59e1e..., anchor count 1 to 0 and injected count 0 to 1. Restored undertrap ... EXIT INT TERMwithgit checkout HEAD -- ABSOLUTE_PATH; restored blob equals the HEAD blob,git diff HEAD --statandgit status --porcelainboth empty. The temporary base-gate copy was removed by the same trap.Two further ablations, same discipline:
'pending-registration'from the widened verdict set (the rule applied verbatim, with no carve-out) reds exactly two rows, and they are exactly the two the ruling named:AMBIGUOUS_METADATA_STEMinpackages/metadataandowd_widening_forbiddeninpackages/plugins/plugin-security. Gate exit 1, restored blob equals HEAD.Acceptance item 5: the negative controls
The two surviving verdicts must not be refused by the widening, or the implementation would be "everything unregistered reds", which passes item 4 while deleting two legitimate exemptions. On the live tree the widened gate is green over all 52 rows — 49
foreign-vocabulary, 1runtime-pinned, 2pending-registration— and the--self-testbattery pins each one outsidepackages/spec/src/as an explicit case, alongside the two boundary controls (a private member'ssrc/and a published package file outsidesrc/, both outside the face).The battery was rewritten and its floor raised from 7 to 21. Every case passes its own
publishedFaces, becausereconciledefaults to none; the last three cases hold the fixture population against the live enumeration, so the battery cannot be green over a workspace this repo does not have.Clause 2 and the changeset, both re-measured
Clause-②: no, re-derived rather than inherited.git grep -n "dispatcher-error-vocabulary" HEAD -- packages/runtime/src/index.tsreturns zero: the module is not exported from the published entry point. Confirmed from the dist side after building@objectstack/runtime:boot-refusal,CodeVerdict,UNREGISTERED_CODE_SITES,SANDBOX_AUTHORED_LIMBanddispatcher-error-vocabularyeach hit 0 of the 6 files inpackages/runtime/dist/, against positive controlsMissingDriverPackageErrorandresolveProjectDatabaseUrlat 4 files each. Deleting a member of a type that does not ship, plus a strictly stricter gate, moves no published face.Re-derived again after the TSDoc edit, because that edit lands in⚠️ The clause-2 enqueue gate's PATH leg will nonetheless fire, because the diff now touches
packages/spec/src/**: the ledger holds the same 282 unique codes before and after it (279 before #16783 landed and added three), every changed line in that file is a TSDoc*line with no code line moved, and the generatedapi-surfaceandspec-changes.jsonartifacts did not move —check:generatedfound only the docs page stale. Prose in a shipped file widens no accept set and adds no key, soClause-②stays no.packages/spec/src/**; that is expected and is the PM's to handle with a contract-review-tier review before enqueue. It is not a reason to declareyes, and the prose was not left false to dodge it.skip-changeset, measured on both halves as the lane requires. Path test: 70 published manifests examined, none carries afiles[]entry that could ship a rootscripts/path, and the root manifest isprivate: true;@objectstack/runtime'sfiles[]is['dist','README.md','CHANGELOG.md'], so itssrc/is not shipped. Dist test: the symbol readings above. Nothing published moves.Verification
Final head
07cb189d.origin/mainmoved twice under this branch and was merged both times throughbash scripts/pm/os-regen-merge.sh; the second merge (b3c14d2a) brought #16783, one of the two PRs that contended this ledger file. Its regions (:677-728) and this branch's (:106-136) are disjoint and the source merged with no conflict; the generated page had both sides changed, so the script restored main's side and step 4 regenerated it (07cb189d), after which the page carries #16783's three new rows and this branch's corrected TSDoc together. Verified after the merge rather than assumed: all fourteen of PR #16879's registrations still present (14/14), ledger at 282 unique codes,boot-refusalstill absent fromCodeVerdict, and this branch'sderivePublishedFacesand corrected TSDoc both intact. All readings below are from07cb189d.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, letting it take the change set from the merge base itself: 111 families over the four changed paths (the TSDoc and docs paths took it from 64 to 111). Re-derived and re-run in full on the final head after the second merge. 110 exit 0. Reconciled with--ran: "111 derived famil(ies) accounted for — 111 run, 0 NOT-MEASURED".check:doc-formula-expressionsandcheck:doc-security-postureexited 3 naming@objectstack/formulaand@objectstack/lintas unbuilt, andcheck:skill-examplesexited 1 asking for@objectstack/client-react— building those three closures took all three to exit 0.pnpm check:dispatcher-error-vocabularyexit 0 — self-test "10 shapes + 344 assertions OK", production run "52 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry (spec: register the dispatcher conformance gate's reported error codes in ERROR_CODE_LEDGER (spec half of the #8087 ruling) #8846)".pnpm check:dual-build-cjs-loadsexit 3,PREREQUISITE NOT MET— it reads built output and some packages have nodist/in this worktree; its own text says "This is NOT a pass: nothing was measured". Recorded as NOT MEASURED, not as a failure, and not as a pass either. Its self-test passed (93 cases). Whole-tree family, CI owns it.scripts/pm/os-verify-lock.sh:pnpm --workspace-concurrency=2 --filter '@objectstack/runtime...' buildexit 0,pnpm --filter @objectstack/runtime typecheckexit 0,pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/actions-validation-envelope.test.tsexit 0. WrapperVERDICT command-exit 0.*.test.ts; its tests are its--self-test, wired intocheck:dispatcher-error-vocabularyand run above.git grepof the script's filename across test files finds one carrier,packages/spec/src/api/error-code-ledger.test.ts— run, 20/20 passed.eslint . --no-inline-configexit 0 over 6398 files, 0 errors, 0 warnings, with both changed files confirmed present in the linted set.Acceptance notes
Not filed, recorded here.
The
error-code-ledger.zod.tsprose is fixed in this PR, not deferred — see the patch-round section above. An earlier revision of this PR argued it could be left because it "describes a case that can no longer arise"; that argument was wrong for:109-113, which describes where a live mechanism lives, and it is withdrawn.Still out of scope and genuinely deferred: the three source comments in
packages/core(×2) andpackages/drivers/driver-sql(×1) that state the pre-ruling position. #16884 is already open on exactly those and explicitly names the staleboot-refusalpointer, so nothing was re-filed. Successor: #16884.packages/spec/src/stack.zod.tsmentions theboot-refusalclass but qualifies itself with "on the tree this landed against", so it stays true as a historical reading and needs no change. Successor: none.The
sandbox-authoredverdict is now refused everywhere in the published face, which follows from "refusing exactly the verdicts the spec face refuses today exceptpending-registration". No row on this tree carries it — the sandbox limb is outside the scan by construction — so this is a widening with no live subject. It was not retired: that is not ruled here.🤖 Generated with Claude Code
https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Generated by Claude Code